a tool for shared writing and social publishing
at feature/backdate 131 lines 4.5 kB view raw
1import { subscribeToPublication } from "app/lish/subscribeToPublication"; 2import { cookies } from "next/headers"; 3import { redirect } from "next/navigation"; 4import { NextRequest, NextResponse } from "next/server"; 5import { createOauthClient } from "src/atproto-oauth"; 6import { setAuthToken } from "src/auth"; 7 8import { supabaseServerClient } from "supabase/serverClient"; 9import { URLSearchParams } from "url"; 10import { 11 ActionAfterSignIn, 12 parseActionFromSearchParam, 13} from "./afterSignInActions"; 14 15type OauthRequestClientState = { 16 redirect: string | null; 17 action: ActionAfterSignIn | null; 18}; 19 20export async function GET( 21 req: NextRequest, 22 props: { params: Promise<{ route: string; handle?: string }> }, 23) { 24 const params = await props.params; 25 let client = await createOauthClient(); 26 switch (params.route) { 27 case "metadata": 28 return NextResponse.json(client.clientMetadata); 29 case "jwks": 30 return NextResponse.json(client.jwks); 31 case "login": { 32 const searchParams = req.nextUrl.searchParams; 33 const handle = searchParams.get("handle") as string; 34 // Put originating page here! 35 let redirect = searchParams.get("redirect_url"); 36 if (redirect) redirect = decodeURIComponent(redirect); 37 let action = parseActionFromSearchParam(searchParams.get("action")); 38 let state: OauthRequestClientState = { redirect, action }; 39 40 // Revoke any pending authentication requests if the connection is closed (optional) 41 const ac = new AbortController(); 42 43 const url = await client.authorize(handle || "https://bsky.social", { 44 scope: "atproto transition:generic transition:email", 45 signal: ac.signal, 46 state: JSON.stringify(state), 47 }); 48 49 return NextResponse.redirect(url); 50 } 51 case "callback": { 52 const params = new URLSearchParams(req.url.split("?")[1]); 53 54 let redirectPath = "/"; 55 try { 56 const { session, state } = await client.callback(params); 57 let s: OauthRequestClientState = JSON.parse(state || "{}"); 58 redirectPath = decodeURIComponent(s.redirect || "/"); 59 let { data: identity } = await supabaseServerClient 60 .from("identities") 61 .select() 62 .eq("atp_did", session.did) 63 .single(); 64 if (!identity) { 65 let existingIdentity = (await cookies()).get("auth_token"); 66 if (existingIdentity) { 67 let data = await supabaseServerClient 68 .from("email_auth_tokens") 69 .select("*, identities(*)") 70 .eq("id", existingIdentity.value) 71 .single(); 72 if (data.data?.identity && data.data.confirmed) 73 await supabaseServerClient 74 .from("identities") 75 .update({ atp_did: session.did }) 76 .eq("id", data.data.identity); 77 78 return handleAction(s.action, redirectPath); 79 } 80 const { data } = await supabaseServerClient 81 .from("identities") 82 .insert({ atp_did: session.did }) 83 .select() 84 .single(); 85 identity = data; 86 } 87 let { data: token } = await supabaseServerClient 88 .from("email_auth_tokens") 89 .insert({ 90 identity: identity!.id, 91 confirmed: true, 92 confirmation_code: "", 93 }) 94 .select() 95 .single(); 96 97 if (token) await setAuthToken(token.id); 98 99 // Process successful authentication here 100 console.log("authorize() was called with state:", state); 101 102 console.log("User authenticated as:", session.did); 103 return handleAction(s.action, redirectPath); 104 } catch (e) { 105 redirect(redirectPath); 106 } 107 } 108 default: 109 return NextResponse.json({ error: "Invalid route" }, { status: 404 }); 110 } 111} 112 113const handleAction = async ( 114 action: ActionAfterSignIn | null, 115 redirectPath: string, 116) => { 117 let parsePath = decodeURIComponent(redirectPath); 118 let url; 119 if (parsePath.includes("://")) url = new URL(parsePath); 120 else url = new URL(decodeURIComponent(redirectPath), "https://example.com"); 121 if (action?.action === "subscribe") { 122 let result = await subscribeToPublication(action.publication); 123 if (result.success && result.hasFeed === false) 124 url.searchParams.set("showSubscribeSuccess", "true"); 125 } 126 127 let path = url.pathname; 128 if (url.search) path += url.search; 129 if (url.hash) path += url.hash; 130 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path); 131};